#! /usr/bin/env python
# -*- coding:utf-8 -*-
import datetime
import logging
import pandas as pd
import mysql_conn
import model_strategic as sa
import load_info as load
import utils_data as util
"""
实现固定策略判断功能，将固定策略写入stg_temp表
总体结构    
    class RelationCondition    #外观类，控制数据，持久化数据，隐藏内部实现
    class RegularCondition     #实际计算类，缓存查询结果，执行计算逻辑
"""
logger = logging.getLogger()
class RelationCondition:
    """Facade Pattern
    简化操作类，信息分类及写入库中"""
    def __init__(self, date_str=None):
        ld = load.Device_load(date_str)
        date_day = ld.load_pre_date(date_str)
        gl = load.Graph_load()
        self.graph_id = gl.put_databydate(date_day.strftime("%Y-%m-%d"))  # 获取target对应日期的图谱ID
        gl.put_databyid(self.graph_id)
        ul = util.Device_util(self.graph_id, date_str)
        self.relation_condition = ul.relation_split()[1]
        self.device_info_list = self.get_query_dict()  # [[{'device_id': 3, 'work_time': '23-23'},{}],[{},{}]]
        # 设备按所属策略分类，及结果
        self.regular_list = []
        self.regular_res = pd.DataFrame()
        self.model_list = []
        self.model_res = pd.DataFrame()
        self.regularcondition = RegularCondition(self.regular_list, self.graph_id, date_day)  # 组合模式
        self.set_day(date_str)
    def get_data_order(self, *args, **kwargs):
        """
        to_db使用，时间向量为0时-23时
        :return:
            list like [[start_time, end_time, power, device_from, device_to, info, graph_id, stgy_type],...]
        """
        self.clean()
        # 分类获得设备，默认不会有模型影响设备，仅条件影响和启动时间
        self.route()
        # 条件影响设备存在
        if self.regular_list:
            self.regular_res = self.regularcondition.get_data_order()
        # 模型计算设备
        if self.model_list:
            # 设备条件设定正常时无此项
            logger.info('err: 固定策略中出现模型计算设备')
        # 天气结果+时间结果为全部固定策略
        return self.regular_res
    def get_data(self, *args, **kwargs):
        """
        run_auto使用，时间向量为17时-16时
        :return: dict{(3,6):[0,0,...], ...}
        """
        self.clean()
        # 分类获得设备，默认不会有模型影响设备，仅条件影响和启动时间
        self.route()
        # 条件影响设备存在
        if self.regular_list:
            self.regular_res = self.regularcondition.get_data()
        # 模型计算设备
        if self.model_list:
            # 设备条件设定正常时无此项
            logger.info('err: 固定策略中出现模型计算设备')
        # 天气结果+时间结果为全部固定策略
        return self.regular_res
    def get_data_dispredict(self, *args, **kwargs):
        """
        获得非策略设备信息，时间向量为17时-16时
        :return:
            dict like {(3, 36):[0, 0, ...], ...():[]}
        """
        self.clean()
        # 分类获得设备，默认不会有模型影响设备，仅条件影响和启动时间
        self.route()
        # 条件影响设备存在
        if self.regular_list:
            self.regular_res = self.regularcondition.get_data_dispredict()
        # 模型计算设备
        if self.model_list:
            # 设备条件设定正常时无此项
            logger.info('err: 固定策略中出现模型计算设备')
        # 天气结果+时间结果为全部固定策略
        return self.regular_res
    def route(self):
        """
        根据用户选定信息二次分类固定策略与时间开启
        :return: None
        """
        for i in self.device_info_list:
            # 2者都选模型计算，则放弃固定策略
            if i[0]['start_button'] == 0 and i[1]['start_button'] == 0:
                self.model_list.append(i)
            # 其它为固定策略
            else:
                self.regular_list.append(i)
    def set_day(self, date):
        """
        设定生成策略默认时间
        :param date:datetime|str
        :return:None
        """
        if isinstance(date, str):
            date = datetime.datetime.strptime(date, '%Y-%m-%d')
        self.regularcondition.tomorrow = date
    def to_db(self):
        res = self.get_data_order()
        # [stime, etime, info, graphid, stgy_type]
        sql = 'INSERT INTO strategy_temp(start_time, end_time, power, device_from, device_to, info, graph_id, stgy_type) VALUES(%s,%s,%s,%s,%s,%s,%s);'
        with mysql_conn.Session() as session:
            session.change_many(sql, res)
        logger.info('写入成功 rows =', len(res))
    def get_query_dict(self):
        """
        从device表获取设备信息,一次查询多条
        :return:
            list like [{dev_info}, {dev_info}, [fromto]]
        """
        # 产生查询设备列表
        tmp = set()
        for i in self.relation_condition['device_from']:
            tmp.add(i)
        for i in self.relation_condition['device_to']:
            tmp.add(i)
        sql = """SELECT device_id,device_name,is_predicted,work_time,start_button,start_condition,start_operator,start_value,
                    end_button,end_condition,end_operator,end_value
                    FROM device WHERE device_id IN (%s"""+",%s"*(len(tmp)-1)+")"
        # 单条查询
        with mysql_conn.Session() as session:
            tmp_result = session.query(sql, list(tmp))
        # 结果格式整理
        res_list = []
        for fromto in zip(self.relation_condition['device_from'], self.relation_condition['device_to']):
            dev1, dev2 = None, None
            for i in tmp_result:
                if i['device_id'] == fromto[0]:
                    dev1 = i
                elif i['device_id'] == fromto[1]:
                    dev2 = i
            if (not dev1) or (not dev2):
                logger.error('当前设备%s,%s不在查询列表中' % (dev1, dev2))
                raise ValueError('device not in querylist')
            res_list.append([dev1, dev2, fromto])
        return res_list
    def get_regular_strategy(self):
        """
        获得固定策略结果（已废弃）
        :return:
        """
        return self.regular_res
    def get_model_strategy(self):
        """
        获得模型预测结果（已废弃）
        :return:
        """
        return self.model_res
    def clean(self):
        """
        清理前次运行结果
        :return: None
        """
        self.regular_list.clear()
        self.model_list.clear()
class RegularCondition:
    """固定策略
    数据计算类，返回二维数据列表"""
    def __init__(self, regular_list, graph_id=-1, date=None):
        # 初始化天气查询函数
        gl = load.Graph_load()
        self.get_weather_hour = gl.load_weather_hour
        self.get_weather_day = gl.load_weather_day
        # 生成日期
        ld = load.Device_load()
        self.tomorrow = ld.load_pre_date(date)
        self.graph_id = graph_id
        lg = load.Graph_load()
        self.stgy_type = lg.load_strategy_type(graph_id)
        self.regular_list = regular_list
        self.graph_worktime = gl.load_graph_worktime(self.graph_id)
        self.worktime = set()
        self.closetime = set()
        col = ['from', 'to']
        col.extend([i for i in range(24)])
        self.result_list = []
        self.result_dict = {}
        self.complement_result_dict = {}
    def get_data_order(self, regular_singleton=None):
        """
        惰性计算，返回0-23时查询结果
        :param regular_singleton:设备信息列表 [{dev_info}, {dev_info},[from, to]]|None
        :return:
            list like [[start_time, end_time, power, device_from, device_to, info, graph_id, stgy_type],...]
        """
        # 清空前一次结果
        self.clean()
        # 缓存天气结果
        self.get_weather_hour(self.tomorrow)
        self.get_weather_day(self.tomorrow)
        # 传入单条或使用初始化时传入一组数据
        regular_list = [regular_singleton] if regular_singleton else self.regular_list

        for res_dict in regular_list:  # res_dict实际格式为字典列表[{dev_info}, {dev_info},[fromto]]
            # 获得是否条件停用，若关闭则整天关闭
            if self.get_closetime(res_dict):
                logger.info(res_dict[2], '按条件整天停用')
                continue
            # 获得设备启用时间
            self.get_worktime(res_dict)  # 结果格式化为[start, ..., end]
            # 获得条件启用时间
            self.get_weathertime(res_dict)
            # 计算结合天气条件后工作时间
            self.worktime = self.weathertime & self.worktime
            # 将worktime转为list,拼接入总结果（2维list）中
            self.data_process_order(self.worktime, res_dict)  # 获得单条结果数据
        return self.result_list
    def get_data_disorder(self, regular_singleton=None):
        """
        惰性计算，返回17-16时查询结果
        :param regular_singleton:设备信息列表 [{dev_info}, {dev_info},[from, to]]|None
        :return:
            list like [[start_time, end_time, power, device_from, device_to, info, graph_id, stgy_type],...]
        """
        # 清空前一次结果
        self.clean()
        # 缓存天气结果
        self.get_weather_hour(self.tomorrow)
        self.get_weather_day(self.tomorrow)
        # 传入单条或使用初始化时传入一组数据
        regular_list = [regular_singleton] if regular_singleton else self.regular_list

        for res_dict in regular_list:  # res_dict实际格式为字典列表[{dev_info}, {dev_info},[fromto]]
            # 获得是否条件停用，若关闭则整天关闭
            if self.get_closetime(res_dict):
                logger.info(res_dict[2], '按条件整天停用')
                continue
            # 获得设备启用时间
            self.get_worktime(res_dict)  # 结果格式化为[start, ..., end]
            # 获得条件启用时间
            self.get_weathertime(res_dict)
            # 计算结合天气条件后工作时间
            self.worktime = self.weathertime & self.worktime
            # 将worktime转为list,拼接入总结果（2维list）中
            self.data_process_disorder(self.worktime, res_dict)  # 获得单条结果数据
        return self.result_list
    def get_data(self, regular_singleton=None):
        """
        惰性计算，返回17-16时计算结果
        :param regular_singleton: 设备信息列表 [{dev_info}, {dev_info},[from, to]]|None
        :return: dict{(3,6):[0,0,...], ...}
        """
        # 清空前一次结果
        self.clean()
        # 缓存天气结果
        self.get_weather_hour(self.tomorrow)
        self.get_weather_day(self.tomorrow)
        # 传入单条或使用初始化时传入一组数据
        regular_list = [regular_singleton] if regular_singleton else self.regular_list
        for res_dict in regular_list:  # res_dict实际格式为字典列表[{dev_info}, {dev_info}, [from, to]]
            # 获得是否条件停用，若关闭则整天关闭
            if self.get_closetime(res_dict):
                logger.info(res_dict[2], '按条件整天停用')
                continue
            # 获得设备启用时间
            self.get_worktime(res_dict)  # 结果格式化为[start, ..., end]
            # 获得条件启用时间
            self.get_weathertime(res_dict)
            # 计算结合天气条件后工作时间
            self.worktime = self.weathertime & self.worktime
            work_start = self.graph_worktime["start_work_time"]
            work_end = self.graph_worktime["end_work_time"]
            self.worktime = set(filter(lambda x: x >= work_start and x < work_end, self.worktime))
            # 将worktime转为dict,拼接入总结果（dict）中
            self.data_process(self.worktime, res_dict)  # 获得单条结果数据
        return self.result_dict
    def get_data_dispredict(self, regular_singleton=None):
        """
        惰性计算，返回不纳入决策查询结果
        :param regular_singleton: 设备信息列表 [{dev_info}, {dev_info},[from, to]]|None
        :return: dict{(3,36):[0,0,..], ...}
        """
        self.get_data(regular_singleton)
        return self.complement_result_dict
    def get_worktime(self, res_dict):
        """
        获得工作时间
        :param res_dict: 设备信息列表 [{dev_info}, {dev_info},[from, to]]
        :return: like set{8,11,17}
        """
        dev_1 = [int(i) for i in res_dict[0]['work_time'].split('-')]  # 起止时间[1-23]
        worktime1 = set([i for i in range(dev_1[0], dev_1[1])])  # 工作时间{1,2...23}
        if not worktime1:
            worktime1 = set([i for i in range(24)])
        dev_2 = [int(i) for i in res_dict[1]['work_time'].split('-')]
        worktime2 = set([i for i in range(dev_2[0], dev_2[1])])
        if not worktime2:
            worktime2 = set([i for i in range(24)])

        if worktime1 is None or worktime2 is None:
            logger.error('部分设备工作时间为空，检查worktime设定异常')
        # worktime交集
        self.worktime = worktime1 & worktime2
    def get_closetime(self, res_dict):
        """
        若self.tomorrow对应日期停用，返回1。不停用则返回0
        :param res_dict: 设备信息列表 [{dev_info}, {dev_info},[from, to]]
        :return: bool
        """
        dev_1 = [res_dict[0]['end_condition'], res_dict[0]['end_operator'], res_dict[0]['end_value']]
        # 任意设备关闭则关系对消失，返回1
        # 若两设备均不启用条件关闭则此方法返回0，即当日不关闭
        if res_dict[0]['end_button'] == 1:
            try:
                weathertime1 = self.weather_query(*dev_1)
            except Exception as e:
                logger.info(res_dict[2], '获取小时表失败，改用天表')
                weathertime1 = self.weather_query_day(*dev_1)
        else:
            weathertime1 = set()

        dev_2 = [res_dict[1]['end_condition'], res_dict[1]['end_operator'], res_dict[1]['end_value']]
        if res_dict[1]['end_button'] == 1:
            try:
                weathertime2 = self.weather_query(*dev_2)
            except Exception:
                weathertime2 = self.weather_query_day(*dev_2)
        else:
            weathertime2 = set()

        # 当天包含停用条件则停用返回1
        if weathertime1 | weathertime2:
            return 1
        else:
            return 0
    def data_process_order(self, worktime, resdict):
        """
        处理数据, 0时-23时格式列表
        :param worktime:工作时间集合set{8,11,17}
        :param resdict:设备信息列表 [{dev_info}, {dev_info},[from, to]]
        :return: 返回值[[starttime, endtime, power, fromid, toid, info, graph_id, stgy_type],...]
        """
        # 筛选is_predict都不是1的情况
        if resdict[0]['is_predicted'] != 1 and resdict[1]['is_predicted'] != 1:
            pass
        else:
            return
        result_tmp = [0 for i in range(24)]
        # 按set中idx将对应位，置1
        for i in worktime:
            result_tmp[i] = 1
        # 生成时间段列表
        time_list = self.get_time(result_tmp)
        # 生成插入数据[stime, etime, info, graphid, stgy_type]
        fromto = resdict[2]
        power = 25  # 固定策略能量为1

        # 生成单行数据，并汇入结果列表
        for i in time_list:
            # 由于天气表爬取时间为时间段，end_time需+1
            i[1] = i[1]+datetime.timedelta(hours=1)
            info = str(i[0].hour) + ':00_' + str(i[1].hour)+':00'
            if resdict[0]['device_id'] == fromto[0]:
                info = info + resdict[0]['device_name'] + '_' + resdict[1]['device_name']
            else:
                info = info + resdict[1]['device_name'] + resdict[0]['device_name']
            i.extend([power, fromto[0], fromto[1], info, self.graph_id, self.stgy_type])
            self.result_list.append(i)
    def data_process_disorder(self, worktime, resdict):
        """
        处理数据17时-16时,格式列表，并加入结果中
        :param worktime: 工作时间集合 like {8，9，12，...，17}仅包含设备工作时间对应int
        :param resdict: 设备信息列表 [{dev_info}, {dev_info},[from, to]]
        :return: None
        """
        result_tmp = [0 for i in range(24)]
        # 按set中idx将对应位，置1
        for i in worktime:
            result_tmp[i] = 1
        # 时间顺序转换
        result_tmp = result_tmp[17:] + result_tmp[:17]
        # 生成时间段列表
        time_list = self.get_time(result_tmp)
        # 生成插入数据[stime, etime, info, graphid, stgy_type]
        fromto = resdict[2]

        # 生成单行数据，并汇入结果列表
        for i in time_list:
            # 由于天气表爬取时间为时间段，end_time需+1
            i[1] = i[1]+datetime.timedelta(hours=1)
            info = str(i[0].hour) + ':00_' + str(i[1].hour)+':00'
            if resdict[0]['device_id'] == fromto[0]:
                info = info + resdict[0]['device_name'] + '_' + resdict[1]['device_name']
            else:
                info = info + resdict[1]['device_name'] + resdict[0]['device_name']
            i.extend([self.graph_id, fromto[0], fromto[1], info, self.stgy_type])
            self.result_list.append(i)
    def data_process(self, worktime, resdict):
        """
        处理数据17时-16时，格式字典like {(3, 36):[0,...,0]},加入结果列表中
        :param worktime: 工作时间集合 like set{8，9，12，...，17}仅包含设备工作时间对应int
        :param resdict: 设备信息列表 [{dev_info}, {dev_info},[from, to]]
        :return: None
        """
        result_tmp = [0 for _ in range(24)]
        # 按set中idx将对应位，置1
        for i in worktime:
            result_tmp[i] = 25
        # 时间顺序转换
        result_tmp = result_tmp[17:] + result_tmp[:17]
        # 生成插入数据[stime, etime, info, graphid, stgy_type]
        fromto = resdict[2]
        # if resdict[0]['device_id'] == fromto[0]:
        #     info = resdict[0]['device_name'] + '_' + resdict[1]['device_name']
        # else:
        #     info = resdict[1]['device_name'] + resdict[0]['device_name']
        # 筛选is_predict都不是1的情况
        if resdict[0]['is_predicted'] != 1 and resdict[1]['is_predicted'] != 1:
            self.result_dict[tuple(fromto)] = result_tmp
        else:
            self.complement_result_dict[tuple(fromto)] = result_tmp
    def weather_query(self, condition, opreator, val):
        """
        查询明天weather_forecast_hourly中天气满足情况
        :param condition: 关闭条件：int
        :param opreator:判断条件：int
        :param val:判断值：str|int
        :return:
            满足条件的小时集合 set{8, 12, 17,...}
        """
        # 传入单设备启用条件
        con2weather = {
            0: 'weather',
            1: 'wind',
            2: 'temperature',
            3: 'humidity'
        }
        opr2symbol = {
            0: '>',
            1: '<',
            2: '==',
            3: '>=',
            4: '<='
        }
        if not self.get_weather_hour:
            logger.error('未生成小时天气缓存')
            raise ValueError('未生成小时天气缓存')
        res = set()
        condition_list = self.get_weather_hour(self.tomorrow)[con2weather[condition]]
        condition_time = self.get_weather_hour(self.tomorrow)['sdate']
        # 判断是否满足设定条件
        if not condition_list:
            logger.error('未生成小时天气缓存')
            raise ValueError('未生成小时天气缓存')
        for i in range(len(condition_list)):
            if eval('condition_list[i] '+opr2symbol[opreator]+' int(val)'):
                res.add(condition_time[i].hour)
        return res
    def weather_query_day(self, condition, opreator, val):
        """
        查询明天weather_forecast_daly中天气满足情况
         :param condition: 关闭条件：int
        :param opreator:判断条件：int
        :param val:判断值：str|int
        :return:
            满足条件的小时集合 set{8, 12, 17,...}
        """
        # 传入单设备启用条件
        con2weather = {
            0: 'weather',
            1: 'wind',
            2: 'temp',
            3: 'humidity_'
        }
        opr2symbol = {
            0: '>',
            1: '<',
            2: '==',
            3: '>=',
            4: '<='
        }
        # 根据输入生成key，确定high、low
        keywd = con2weather[condition]
        if condition in (2, 3):
            if opreator in (1, 3):
                keywd += 'high'
            else:
                keywd += 'low'
        if self.get_weather_day is None or not self.get_weather_day(self.tomorrow)[keywd]:
            logger.error('未生成日天气缓存')
            raise ValueError('未生成日天气缓存')
        res = set()
        # 判断是否满足设定条件
        if eval('self.get_weather_day(self.tomorrow)[keywd] ' + opr2symbol[opreator] + ' int(val)'):
            res = {1}
        return res
    def get_time(self, alist):
        """
        根据输入时间布尔列表返回时间段列表
        :param alist: list [int x 24]
        :return:
            时间段起止时间列表
            [[datetime, datetime]，...]
        """
        tmp = 0
        flag = 0
        res = []
        tmp_list = []
        tomorrow = self.tomorrow
        for i in alist:
            time = tomorrow.replace(hour=tmp)
            if i and not flag:
                tmp_list.append(time)
                start = time
                flag = 1
            if not i and flag:
                tmp_list.append(tomorrow.replace(hour=tmp - 1))
                res.append(tmp_list)
                tmp_list = []
                flag = 0
            tmp += 1
        if alist[-1]:
            res.append([start, tomorrow.replace(hour=tmp - 1)])
        return res

        # 两设备条件启用时间交集
    def get_weathertime(self, res_dict):
        """
        获得天气启用时间（设备1与设备2交集）
        :param res_dict:设备信息列表 [{dev_info}, {dev_info},[from, to]]
        :return: 满足条件时间集合 set {8，11，17}
        """
        dev_1 = [res_dict[0]['start_condition'], res_dict[0]['start_operator'], res_dict[0]['start_value']]
        # 单设备未选择条件开启则返回全集set([0,...,23])
        if res_dict[0]['start_button'] == 1:
            weathertime1 = self.weather_query(*dev_1)
        else:
            weathertime1 = set([i for i in range(24)])

        dev_2 = [res_dict[1]['start_condition'], res_dict[1]['start_operator'], res_dict[1]['start_value']]
        if res_dict[1]['start_button'] == 1:
            weathertime2 = self.weather_query(*dev_2)
        else:
            weathertime2 = set([i for i in range(24)])

        if weathertime1 is None or weathertime2 is None:
            logger.info('部分设备全天无满足启用条件')
        # weathertime交集
        self.weathertime = weathertime1 & weathertime2
    def set_day(self, date):
        """
        设定类参数self.tomorrow，决定默认生成策略日期
        :param date: datetime|str, fmt('%Y-%m-%d')
        :return: None
        """
        if isinstance(date, str):
            date = datetime.datetime.strptime(date, '%Y-%m-%d')
        self.tomorrow = date
    def clean(self):
        """
        清理前次预测结果
        :return: None
        """
        self.result_list = []
        self.result_dict = {}
        self.complement_result_dict = {}
def clean_test_table():
    """测试用辅助函数，清空临时表"""
    with mysql_conn.Session() as s:
        s.change('TRUNCATE TABLE strategy_temp')

if __name__ == '__main__':
    # clean_test_table()
    relation = RelationCondition('2022-01-16')
    print(relation.get_data())
